
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id$
 */

package org.dellroad.sidekar.datastore.table;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.dellroad.sidekar.Entity;
import org.dellroad.sidekar.Index;
import org.dellroad.sidekar.ListField;
import org.dellroad.sidekar.MapField;
import org.dellroad.sidekar.SetField;
import org.dellroad.sidekar.SimpleField;
import org.dellroad.sidekar.datastore.AbstractDatastore;
import org.dellroad.sidekar.datastore.Datastore;
import org.dellroad.sidekar.query.QueryList;
import org.dellroad.sidekar.query.QueryMap;
import org.dellroad.sidekar.query.QuerySet;
import org.dellroad.sidekar.schema.SchemaModificationException;
import org.dellroad.sidekar.schema.change.AddEntityChange;
import org.dellroad.sidekar.schema.change.AddFieldChange;
import org.dellroad.sidekar.schema.change.AddIndexChange;
import org.dellroad.sidekar.schema.change.EntityAbstractnessChange;
import org.dellroad.sidekar.schema.change.RemoveEntityChange;
import org.dellroad.sidekar.schema.change.RemoveFieldChange;
import org.dellroad.sidekar.schema.change.RemoveIndexChange;
import org.dellroad.sidekar.schema.change.RenameEntityChange;
import org.dellroad.sidekar.schema.change.SchemaChange;
import org.dellroad.sidekar.schema.change.SchemaChangeSwitch;
import org.dellroad.sidekar.schema.model.ComplexFieldModel;
import org.dellroad.sidekar.schema.model.EntityModel;
import org.dellroad.sidekar.schema.model.FieldModel;
import org.dellroad.sidekar.schema.model.FieldModelSwitchAdapter;
import org.dellroad.sidekar.schema.model.IndexModel;
import org.dellroad.sidekar.schema.model.IntegerFieldModel;
import org.dellroad.sidekar.schema.model.ListFieldModel;
import org.dellroad.sidekar.schema.model.LongFieldModel;
import org.dellroad.sidekar.schema.model.MapFieldModel;
import org.dellroad.sidekar.schema.model.SchemaModel;
import org.dellroad.sidekar.schema.model.SetFieldModel;
import org.dellroad.sidekar.schema.model.SimpleFieldModel;
import org.dellroad.sidekar.schema.model.StringFieldModel;
import org.dellroad.sidekar.util.SidekarException;

/**
 * Support superclass for {@link Datastore} implementations that are based on the notion of tables with rows, columns,
 * and support for indexes (possibly unique) on one or more columns.
 */
public abstract class AbstractTableDatastore extends AbstractDatastore implements SchemaChangeSwitch {

    public static final String DEFAULT_ID_COLUMN_NAME = "id";
    public static final String DEFAULT_TYPE_COLUMN_NAME = "type";
    public static final String DEFAULT_KEY_COLUMN_NAME = "key";
    public static final String DEFAULT_VALUE_COLUMN_NAME = "value";
    public static final String DEFAULT_LIST_INDEX_COLUMN_NAME = "index";
    public static final String DEFAULT_COLLECTION_INDEX_NAME = "element";

    @Override
    protected SchemaModel doSchemaRetrieve() {
        // TODO
        return null;
    }

    @Override
    protected final void doSchemaChange(SchemaChange change) {
        change.visit(this);
    }

    @Override
    protected long doInstanceCreate(Entity<?> entity) {
        // TODO: add row
        return 0;
    }

    @Override
    protected void doInstanceAttach(Entity<?> entity, long id) {
        // TODO: add row
    }

    @Override
    protected void doInstanceDetach(Entity<?> entity, long id) {
        // TODO: remove row
    }

    @Override
    protected boolean doInstanceIsAttached(Entity<?> entity, long id) {
        // TODO: detect row
        return false;
    }

    @Override
    protected <E> QuerySet<E> doEntityAccess(Entity<?> entity, boolean exact) {
        // TODO: list rows
        return null;
    }

    @Override
    protected <T> T doSimpleFieldRead(Entity<?> entity, long id, SimpleField<T> field) {
        // TODO: read row & column
        return null;
    }

    @Override
    protected <T> void doSimpleFieldWrite(Entity<?> entity, long id, SimpleField<T> field, T value) {
        // TODO: write row & column
    }

    @Override
    protected <E> QuerySet<E> doSetFieldAccess(Entity<?> entity, long id, SetField<E> field) {
        // TODO: read rows
        return null;
    }

    @Override
    protected <K, V> QueryMap<K, V> doMapFieldAccess(Entity<?> entity, long id, MapField<K, V> field) {
        // TODO: read rows
        return null;
    }

    @Override
    protected <E> QueryList<E> doListFieldAccess(Entity<?> entity, long id, ListField<E> field) {
        // TODO: read rows
        return null;
    }

    @Override
    protected <E> QuerySet<E> doIndexAccess(Entity<?> entity, long id, Index<E> index, Object... values) {
        // TODO: read rows with values
        return null;
    }

// Schema change methods

    @Override
    public void caseAddEntityChange(AddEntityChange change) {

        // If entity has a parent, then the table exists already
        if (change.getParentName() != null)
            return;

        // Add table
        String tableName = this.getEntityTableName(change.getEntityName());
        this.addTable(tableName);

        // Add ID and type columns
        String idColumnName = this.getIdColumnName(tableName);
        String typeColumnName = this.getTypeColumnName(tableName);
        this.addColumn(tableName, idColumnName, new LongFieldModel());
        this.addColumn(tableName, typeColumnName, new StringFieldModel());

        // Add indexes on the ID and type columns
        this.addIndex(tableName, idColumnName, true, idColumnName);
        this.addIndex(tableName, typeColumnName, true, typeColumnName);
    }

    @Override
    public void caseRemoveEntityChange(RemoveEntityChange change) {

        // Get the current schema and find entity
        SchemaModel schemaModel = this.schemaRetrieve();
        EntityModel entityModel = schemaModel.getEntityModels().get(change.getEntityName());
        if (entityModel == null)
            throw new SchemaModificationException("no entity `" + change.getEntityName() + "' found");

        // TODO: Verify there are no exact references to the entity

        // If the entity has a parent, leave the table there
        if (entityModel.getParentModel() != null)
            return;

        // Remove indexes on ID and type columns
        String tableName = this.getEntityTableName(change.getEntityName());
        this.removeIndex(tableName, this.getIdColumnName(tableName));
        this.removeIndex(tableName, this.getTypeColumnName(tableName));

        // Remove ID and type columns
        this.removeColumn(tableName, this.getIdColumnName(tableName));
        this.removeColumn(tableName, this.getTypeColumnName(tableName));

        // Remove the table
        this.removeTable(tableName);
    }

    @Override
    public void caseRenameEntityChange(RenameEntityChange change) {

        // Get the current schema and find entity and its root ancestor
        SchemaModel schemaModel = this.schemaRetrieve();
        EntityModel entityModel = schemaModel.getEntityModels().get(change.getEntityName());
        if (entityModel == null)
            throw new SchemaModificationException("no entity `" + change.getEntityName() + "' found");
        EntityModel rootEntityModel = entityModel.getRootEntityModel();

        // Update type column values
        String tableName = this.getEntityTableName(rootEntityModel.getName());
        this.changeTypeColumnValues(tableName, this.getEntityTypeColumnValue(change.getEntityName()),
          this.getEntityTypeColumnValue(change.getNewEntityName()));

        // If the entity is the root entity, rename the table too
        if (entityModel == rootEntityModel)
            this.renameTable(tableName, this.getEntityTableName(change.getNewEntityName()));
    }

    @Override
    public void caseEntityAbstractnessChange(EntityAbstractnessChange change) {

        // Get the current schema and find entity
        SchemaModel schemaModel = this.schemaRetrieve();
        EntityModel entityModel = schemaModel.getEntityModels().get(change.getEntityName());
        if (entityModel == null)
            throw new SchemaModificationException("no entity `" + change.getEntityName() + "' found");

        // TODO: If changing to abstract, verify there are no exact references to the entity
        // if (change.isAbstract()) ...
    }

    @Override
    public void caseAddFieldChange(final AddFieldChange change) {
        final String entityName = change.getEntityName();
        final String entityTableName = this.getEntityTableName(entityName);
        final String idColumnName = this.getIdColumnName(entityTableName);
        final String valueColumnName = this.getValueColumnName();
        change.getFieldModel().visit(new FieldModelSwitchAdapter<Void>() {
            @Override
            protected Void caseSimpleFieldModel(SimpleFieldModel fieldModel) {
                AbstractTableDatastore.this.addColumn(entityTableName,
                  AbstractTableDatastore.this.getFieldColumnName(fieldModel.getName()), fieldModel);
                return null;
            }
            @Override
            public Void caseListFieldModel(ListFieldModel fieldModel) {
                final String tableName = this.createComplexTable(fieldModel, fieldModel.getElementModel());
                final String indexColumnName = AbstractTableDatastore.this.getListIndexColumnName();
                AbstractTableDatastore.this.addColumn(tableName, indexColumnName, new IntegerFieldModel());
                AbstractTableDatastore.this.addIndex(tableName, indexColumnName, true, idColumnName, indexColumnName);
                return null;
            }
            @Override
            public Void caseMapFieldModel(MapFieldModel fieldModel) {
                final String tableName = this.createComplexTable(fieldModel, fieldModel.getValueModel());
                final String keyColumnName = AbstractTableDatastore.this.getMapKeyColumnName();
                AbstractTableDatastore.this.addColumn(tableName, keyColumnName, fieldModel.getKeyModel());
                AbstractTableDatastore.this.addIndex(tableName, keyColumnName, true, idColumnName, keyColumnName);
                return null;
            }
            @Override
            public Void caseSetFieldModel(SetFieldModel fieldModel) {
                final String tableName = this.createComplexTable(fieldModel, fieldModel.getElementModel());
                AbstractTableDatastore.this.addIndex(tableName, valueColumnName, true, idColumnName, valueColumnName);
                return null;
            }
            private String createComplexTable(ComplexFieldModel fieldModel, SimpleFieldModel valueModel) {
                final String tableName = AbstractTableDatastore.this.getComplexFieldTableName(entityName, fieldModel);
                AbstractTableDatastore.this.addTable(tableName);
                AbstractTableDatastore.this.addColumn(tableName, idColumnName, new LongFieldModel());
                AbstractTableDatastore.this.addColumn(tableName, valueColumnName, valueModel);
                return tableName;
            }
            @Override
            protected Void caseDefault(FieldModel fieldModel) {
                throw new RuntimeException("internal error: unexpected case");
            }
        });
    }

    @Override
    public void caseRemoveFieldChange(final RemoveFieldChange change) {
        final String entityName = change.getEntityName();
        final String fieldName = change.getFieldName();
        final String entityTableName = this.getEntityTableName(entityName);
        final String idColumnName = this.getIdColumnName(entityTableName);
        final String valueColumnName = this.getValueColumnName();

        // Get the current schema and find entity and field
        SchemaModel schemaModel = this.schemaRetrieve();
        EntityModel entityModel = schemaModel.getEntityModels().get(entityName);
        if (entityModel == null)
            throw new SchemaModificationException("no entity `" + entityName + "' found");
        FieldModel fieldModel = entityModel.getFieldModels().get(fieldName);
        if (fieldModel == null)
            throw new SchemaModificationException("no field `" + fieldName + "' found in entity `" + entityName + "'");

        // Remove the field
        fieldModel.visit(new FieldModelSwitchAdapter<Void>() {
            @Override
            protected Void caseSimpleFieldModel(SimpleFieldModel fieldModel) {
                AbstractTableDatastore.this.removeColumn(entityTableName,
                  AbstractTableDatastore.this.getFieldColumnName(fieldModel.getName()));
                return null;
            }
            @Override
            public Void caseListFieldModel(ListFieldModel fieldModel) {
                final String tableName = this.getTableName(fieldModel);
                final String indexColumnName = AbstractTableDatastore.this.getListIndexColumnName();
                AbstractTableDatastore.this.removeIndex(tableName, indexColumnName);
                AbstractTableDatastore.this.removeColumn(tableName, indexColumnName);
                this.cleanupComplexField(fieldModel);
                return null;
            }
            @Override
            public Void caseMapFieldModel(MapFieldModel fieldModel) {
                final String tableName = this.getTableName(fieldModel);
                final String keyColumnName = AbstractTableDatastore.this.getMapKeyColumnName();
                AbstractTableDatastore.this.removeIndex(tableName, keyColumnName);
                AbstractTableDatastore.this.removeColumn(tableName, keyColumnName);
                this.cleanupComplexField(fieldModel);
                return null;
            }
            @Override
            public Void caseSetFieldModel(SetFieldModel fieldModel) {
                final String tableName = this.getTableName(fieldModel);
                String valueColumnName = AbstractTableDatastore.this.getValueColumnName();
                AbstractTableDatastore.this.removeIndex(tableName, valueColumnName);
                this.cleanupComplexField(fieldModel);
                return null;
            }
            private void cleanupComplexField(ComplexFieldModel fieldModel) {
                final String tableName = this.getTableName(fieldModel);
                AbstractTableDatastore.this.removeColumn(tableName, valueColumnName);
                AbstractTableDatastore.this.removeColumn(tableName, idColumnName);
                AbstractTableDatastore.this.removeTable(tableName);
            }
            private String getTableName(ComplexFieldModel complexFieldModel) {
                return AbstractTableDatastore.this.getComplexFieldTableName(entityName, complexFieldModel);
            }
            @Override
            protected Void caseDefault(FieldModel fieldModel) {
                throw new RuntimeException("internal error: unexpected case");
            }
        });
    }

    @Override
    public void caseAddIndexChange(AddIndexChange change) {

        // Get the current schema and find entity and its root ancestor
        SchemaModel schemaModel = this.schemaRetrieve();
        EntityModel entityModel = schemaModel.getEntityModels().get(change.getEntityName());
        if (entityModel == null)
            throw new SchemaModificationException("no entity `" + change.getEntityName() + "' found");
        EntityModel rootEntityModel = entityModel.getRootEntityModel();

        // Add index to root entity
        IndexModel indexModel = change.getIndexModel();
        this.addIndex(this.getEntityTableName(rootEntityModel.getName()), indexModel.getName(),
          indexModel.isUnique(), this.getColumnNames(indexModel.getFieldNames()));
    }

    @Override
    public void caseRemoveIndexChange(RemoveIndexChange change) {

        // Get the current schema and find entity and its root ancestor
        SchemaModel schemaModel = this.schemaRetrieve();
        EntityModel entityModel = schemaModel.getEntityModels().get(change.getEntityName());
        if (entityModel == null)
            throw new SchemaModificationException("no entity `" + change.getEntityName() + "' found");
        EntityModel rootEntityModel = entityModel.getRootEntityModel();

        // Remove index
        this.removeIndex(this.getEntityTableName(rootEntityModel.getName()), change.getIndexName());
    }

// Subclass required methods

    /**
     * Create a new table.
     *
     * @param name table name
     */
    protected abstract void addTable(String name);

    /**
     * Destroy a table. When this method is invoked, the table will already contain zero columns.
     *
     * @param name table name
     */
    protected abstract void removeTable(String name);

    /**
     * Rename a table.
     *
     * @param name table name
     * @param newName new table name
     */
    protected abstract void renameTable(String name, String newName);

    /**
     * Change type discriminator values. All rows in the named table having {@code oldType} in the
     * {@linkplain #getTypeColumnName(String) type discriminator column} should have that value
     * changed to {@code newType}.
     *
     * @param tableName table name
     * @param oldType previoud type discriminator value
     * @param newType new type discriminator value
     */
    protected abstract void changeTypeColumnValues(String tableName, String oldType, String newType);

    /**
     * Add a column to an existing table.
     *
     * @param tableName table name
     * @param columnName column name
     * @param fieldModel column type
     */
    protected abstract void addColumn(String tableName, String columnName, SimpleFieldModel fieldModel);

    /**
     * Remove a column from an existing table. When this method is invoked, there is guaranteed not to exist
     * any indexes on the column being removed.
     *
     * @param tableName table name
     * @param columnName column name
     */
    protected abstract void removeColumn(String tableName, String columnName);

    /**
     * Add an index.
     *
     * @param tableName table name
     * @param indexName index name
     * @param unique whether index is unique
     * @param columnNames one or more column names (in index order)
     */
    protected abstract void addIndex(String tableName, String indexName, boolean unique, String... columnNames);

    /**
     * Remove an index.
     *
     * @param tableName table name
     * @param indexName index name
     */
    protected abstract void removeIndex(String tableName, String indexName);

// Naming methods

    /**
     * Get the name for a root entity table.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} returns the entity name.
     *
     * @param entityName the root entity's name
     * @return table name
     */
    protected String getEntityTableName(String entityName) {
        return entityName;
    }

    /**
     * Get the name for a simple field column.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} returns the field name.
     *
     * @param fieldName the field name
     * @return column name
     */
    protected String getFieldColumnName(String fieldName) {
        return fieldName;
    }

    /**
     * Get the name for the map table column that holds the key.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} always returns {@link #DEFAULT_KEY_COLUMN_NAME}.
     *
     * @return column name
     */
    protected String getMapKeyColumnName() {
        return AbstractTableDatastore.DEFAULT_KEY_COLUMN_NAME;
    }

    /**
     * Get the name for the collection table column that holds a value.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} always returns {@link #DEFAULT_VALUE_COLUMN_NAME}.
     *
     * @return column name
     */
    protected String getValueColumnName() {
        return AbstractTableDatastore.DEFAULT_VALUE_COLUMN_NAME;
    }

    /**
     * Get the name for the list table column that holds the list index.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} always returns {@link #DEFAULT_LIST_INDEX_COLUMN_NAME}.
     *
     * @return column name
     */
    protected String getListIndexColumnName() {
        return AbstractTableDatastore.DEFAULT_LIST_INDEX_COLUMN_NAME;
    }

    /**
     * Get the name for the entity ID column.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} returns {@link #DEFAULT_ID_COLUMN_NAME}.
     *
     * @param tableName the name of the entity table
     * @return column name
     */
    protected String getIdColumnName(String tableName) {
        return AbstractTableDatastore.DEFAULT_ID_COLUMN_NAME;
    }

    /**
     * Get the name for the entity type discriminator column.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} returns {@link #DEFAULT_TYPE_COLUMN_NAME}.
     *
     * @param tableName the name of the entity table
     * @return column name
     */
    protected String getTypeColumnName(String tableName) {
        return AbstractTableDatastore.DEFAULT_TYPE_COLUMN_NAME;
    }

    /**
     * Get the name for a collection field value table.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} concatenates the entity and field
     * names, separated by an underscore character.
     *
     * @param entityName the name of the entity containing the collection field
     * @param fieldModel the collection field
     * @return column name
     */
    protected String getComplexFieldTableName(String entityName, ComplexFieldModel fieldModel) {
        return entityName + "_" + fieldModel.getName();
    }

    /**
     * Get the name for the index created on collections tables.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} returns {@link #DEFAULT_COLLECTION_INDEX_NAME}.
     *
     * @return index name
     */
    protected String getColectionIndexName() {
        return AbstractTableDatastore.DEFAULT_COLLECTION_INDEX_NAME;
    }

    /**
     * Get value describing the given entity in the type discriminator column.
     *
     * <p>
     * The implementation in {@link AbstractTableDatastore} just returns the entity name.
     *
     * @param entityName the name of the entity
     * @return type discriminator column value
     */
    protected String getEntityTypeColumnValue(String entityName) {
        return entityName;
    }

// Utility methods

    // Add an ID column
    private String addIdColumn(Map<String, SimpleFieldModel> columns, String entityName) {
        String idColumnName = this.getIdColumnName(this.getEntityTableName(entityName));
        this.addColumn(columns, entityName, idColumnName, new LongFieldModel());
        return idColumnName;
    }

    // Add a column to a column map
    private void addColumn(Map<String, SimpleFieldModel> map, String entityName, String name, SimpleFieldModel fieldModel) {
        SimpleFieldModel otherFieldModel = map.put(name, fieldModel);
        if (otherFieldModel != null) {
            throw new SidekarException("the table for entity " + entityName
              + " contains two columns with the same name `" + name + "'");
        }
    }

    // Convert field names into column names
    private String[] getColumnNames(List<String> fieldNames) {
        ArrayList<String> columnNames = new ArrayList<String>(fieldNames.size());
        for (String fieldName : fieldNames)
            columnNames.add(this.getFieldColumnName(fieldName));
        return columnNames.toArray(new String[columnNames.size()]);
    }
}

